﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Moq;

namespace NUnit.Framework
{
    public abstract class specification_fixture
    {
        protected Exception CaughtException;
        protected virtual void Given() { }
        protected abstract void When();
        protected virtual void Finally() { }

        [Given]
        public void Setup()
        {
            CaughtException = new ThereWasNoExceptionButOneWasExpectedException();
            Given();

            try
            {
                When();
            }
            catch (Exception exception)
            {
                CaughtException = exception;
            }
            finally
            {
                Finally();
            }
        }
    }

    [Specification]
    public abstract class specification_fixture<TSubjectUnderTest> where TSubjectUnderTest : class
    {
        private static readonly MethodInfo CreateFuncMethod;

        static specification_fixture()
        {
            CreateFuncMethod = typeof(specification_fixture<TSubjectUnderTest>).GetMethod("CreateFunc");
        }

        private Dictionary<Type, object> _mocks;

        protected Dictionary<Type, object> DoNotMock;
        protected TSubjectUnderTest SubjectUnderTest;
        protected Exception CaughtException;
        protected virtual void SetupDependencies() { }
        protected virtual void Given() { }
        protected abstract void When();
        protected virtual void Finally() { }
        protected List<string> ChangedProperties;

        [Given]
        public void Setup()
        {
            _mocks = new Dictionary<Type, object>();
            DoNotMock = new Dictionary<Type, object>();
            CaughtException = new ThereWasNoExceptionButOneWasExpectedException();

            BuildMocks();
            SetupDependencies();
            SubjectUnderTest = BuildSubjectUnderTest();

            Given();

            ChangedProperties = new List<string>();
            var inpc = SubjectUnderTest as INotifyPropertyChanged;
            if (inpc != null)
                inpc.PropertyChanged += (sender, e) => ChangedProperties.Add(e.PropertyName);

            try
            {
                When();
            }
            catch (Exception exception)
            {
                CaughtException = exception;
            }
            finally
            {
                Finally();
            }
        }

        public Mock<TType> OnDependency<TType>() where TType : class
        {
            return (Mock<TType>)_mocks[typeof(TType)];
        }

        protected void AddMock<T>(Mock<T> mock) where T : class
        {
            _mocks.Add(typeof(T), mock);
        }

        protected bool HasMock<T>()
        {
            return _mocks.ContainsKey(typeof(T));
        }

        private TSubjectUnderTest BuildSubjectUnderTest()
        {
            var constructorInfo = typeof(TSubjectUnderTest).GetConstructors().First();

            var parameters = new List<object>();
            foreach (var mock in _mocks)
            {
                object theObject;
                if (!DoNotMock.TryGetValue(mock.Key, out theObject))
                {
                    if (mock.Value is Mock)
                        theObject = ((Mock)mock.Value).Object;
                    else
                        theObject = mock.Value;
                }

                parameters.Add(theObject);
            }

            return (TSubjectUnderTest)constructorInfo.Invoke(parameters.ToArray());
        }

        private void BuildMocks()
        {
            var constructorInfo = typeof(TSubjectUnderTest).GetConstructors().First();

            foreach (var parameter in constructorInfo.GetParameters())
            {
                if (_mocks.ContainsKey(parameter.ParameterType)) continue;
                _mocks.Add(parameter.ParameterType, CreateMock(parameter.ParameterType));
            }
        }

        private object CreateMock(Type type)
        {
            bool isFunc = false;
            if (type.GetGenericArguments().Count() > 0 && type.GetGenericTypeDefinition() == typeof(Func<>))
            {
                isFunc = true;
                type = type.GetGenericArguments()[0];
            }

            var constructorInfo = typeof(Mock<>).MakeGenericType(type).GetConstructors().First();
            var mock = constructorInfo.Invoke(new object[] { });

            if (isFunc)
            {
                var closedCreateFunc = CreateFuncMethod.MakeGenericMethod(type);
                var func = closedCreateFunc.Invoke(this, new[] { mock });
                return func;
            }

            return mock;
        }

        public Func<T> CreateFunc<T>(Mock<T> mock) where T : class
        {
            return () =>
            {
                if (!_mocks.ContainsKey(typeof(T)))
                    _mocks.Add(typeof(T), mock);
                return mock.Object;
            };
        }
    }

    public class SpecificationAttribute : TestFixtureAttribute { }
    public class GivenAttribute : SetUpAttribute { }
    public class ThenAttribute : TestAttribute { }

    public class ThereWasNoExceptionButOneWasExpectedException : Exception { }
}